/*
 * Copyright 2011-2012 by The Regents of the University of California Licensed
 * under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. you may obtain a copy of the
 * License from
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package edu.ucsd.db.bassarisk.schema;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import edu.ucsd.db.bassarisk.exception.InvalidOperationException;
import edu.ucsd.db.bassarisk.exception.SchemaValidationException;
import edu.ucsd.db.bassarisk.query.Constants;
import edu.ucsd.db.bassarisk.schema.Schema.SequenceRelationship;
import edu.ucsd.db.bassarisk.util.BooleanFlags;

public abstract class DataSequenceClass {
  public static final AttributeClass COMPUTED = new AttributeClass("_COMPUTED_");
  private static final Logger LOG = Logger.getLogger(DataSequenceClass.class
      .getName());
  public static final RunClass POSITION_SPACE =
      new RunClass(Constants.PositionSpaceFlag);

  public static DataSequenceClass findClass(final Schema schema, final String name)
      throws SchemaValidationException {
    if (name.equals(Constants.PositionSpaceFlag)) { //
      return POSITION_SPACE;
    }
    Collection<SchemaAttributeNode> attrs =
        schema.collectAttributeNodesForName(name);
    Collection<SchemaRunNode> runs = schema.collectRunNodesForName(name);
    Set<SchemaAttributeNode> attrsSet = new HashSet<SchemaAttributeNode>();
    Set<SchemaRunNode> runsSet = new HashSet<SchemaRunNode>();
    attrsSet.addAll(attrs);
    runsSet.addAll(runs);
    if (runs.size() > 0 && attrs.size() > 0) {
      String msg = "Invalid schema: both runs and attributes defined with name %s";
      msg = String.format(msg, name);
      throw new SchemaValidationException(msg);
    }
    if (runs.size() > 0) { return new RunClass(name, runsSet); }
    if (attrs.size() > 0) { return new AttributeClass(name, attrsSet); }
    String msg = "Data sequence name %s not found in schema";
    msg = String.format(msg, name);
    throw new SchemaValidationException(msg);
  }

  protected int fullClassSize;
  protected boolean isInitialized;
  protected boolean isRefined;
  protected final String name;
  protected int refinedClassSize;

  protected DataSequenceClass(final String name) {
    this.name = name;
  }

  /**
   * Equality between run classes is based only on their names and not on their
   * refinements.
   */
  @Override
  public boolean equals(final Object other) {
    if (!(other instanceof DataSequenceClass)) { return false; }
    return this.name.equals(((DataSequenceClass) other).name);
  }

  public String getName() {
    return this.name;
  }

  public abstract Collection<? extends SchemaNode> getRefinement();

  public abstract int initailize(Schema schema) throws SchemaValidationException;

  public boolean isInitialized() {
    return this.isInitialized;
  }

  public abstract boolean isPositionSpace();

  public boolean isRefined() {
    return this.isRefined;
  }

  public abstract DataSequenceClass
      makeUnion(List<DataSequenceClass> unionedSequenceClasses);

  protected abstract int refinementSize();

  private void
      refineTo(final DataSequenceClass other, final boolean limitToChildren)
          throws SchemaValidationException {
    SequenceRelationship rela = this.relationshipWith(other);
    // Copy the refinement so we can iterate and remove
    List<SchemaNode> classA = new ArrayList<SchemaNode>();
    classA.addAll(this.getRefinement());
    switch (rela) {
    case AllContainedBy:
    case SomeContainedBy:
      // Remove those subclasses from this class that are not contained within
      // a subclass of the refinement of other.
      loopA: for (SchemaNode subclassA : classA) {
        for (SchemaNode subclassB : other.getRefinement()) {
          if (subclassB.contains(subclassA)) {
            if (limitToChildren) {
              if (subclassA.getParentRun() == subclassB) {
                continue loopA;
              }
            } else {
              continue loopA;
            }
          }
        }
        this.getRefinement().remove(subclassA);
      }
      this.refinedClassSize = this.refinementSize();
      break;
    case ContainsAll:
    case ContainsSome:
      // Remove those subclasses from this class that do not contain at least
      // one subclass from the refinement of other.
      loopA: for (SchemaNode subclassA : classA) {
        for (SchemaNode subclassB : other.getRefinement()) {
          if (subclassA.contains(subclassB)) {
            continue loopA;
          }
        }
        this.getRefinement().remove(subclassA);
      }
      this.refinedClassSize = this.refinementSize();
      break;
    default:
      return;
    }
  }

  public void refineToChildrenOf(final DataSequenceClass other)
      throws SchemaValidationException {
    this.refineTo(other, true);
  }

  /**
   * Remove from this dataSequenceClass all subclasses that do not contain some
   * subclass from another dataSequenceClass.
   */
  public void refineToDescendantsOf(final DataSequenceClass other)
      throws SchemaValidationException {
    this.refineTo(other, false);
  }

  /**
   * There can be several relationships between this on-ordered pair of run
   * names, A and B, in the schema:
   * <ol>
   * <li>A contains B (resp B contains A) :<br/>
   * All subclasses of A (resp B) are strictly contained by some subclass of B
   * (resp A).
   * <li>A partially contains B, (resp B partially contains A) : <br/>
   * All subclasses of A (resp B) are either strictly contained by some subclass
   * of B (resp A), or strictly excluded from all subclasses of B (resp A).
   * <li>A excludes B (resp B excludes A): <br/>
   * All subclasses of A (resp B) are either strictly excluded from all
   * subclasses of B (resp A)
   * </ol>
   * We can also detect several *invalid cases*:
   * <ul>
   * <li>Some runs of class A (resp B) are contained by class B (resp A), but
   * others are neither strictly contained by any nor strictly excluded from all
   * subclasses of B (resp A). In this case, the schema is not "orderable".
   * <ul>
   * 
   * @throws SchemaValidationException
   **/
  public SequenceRelationship relationshipWith(final DataSequenceClass other)
      throws SchemaValidationException {
    if ((!this.isInitialized) || (!other.isInitialized)) {
      String msg = "Method only supported on two schema-initializerd run-classes.";
      LOG.severe(msg);
      throw new InvalidOperationException(msg);
    }
    if (this.isPositionSpace() || other.isPositionSpace()) {
      if (this.isPositionSpace() && other.isPositionSpace()) {
        return SequenceRelationship.Orthogonal;
      } else if (this.isPositionSpace()) {
        return SequenceRelationship.SomeContainedBy;
      } else if (other.isPositionSpace()) { //
        return SequenceRelationship.ContainsSome;
      }
    }
    if (this.name.equals(other.name)) { return SequenceRelationship.Equals; }
    List<SchemaNode> classA = new ArrayList<SchemaNode>();
    List<SchemaNode> classB = new ArrayList<SchemaNode>();
    classA.addAll(this.getRefinement());
    classB.addAll(other.getRefinement());
    BooleanFlags containedBySomeB = new BooleanFlags(classA.size(), false);
    BooleanFlags excludedByAllB = new BooleanFlags(classA.size(), true);
    BooleanFlags containedBySomeA = new BooleanFlags(classB.size(), false);
    BooleanFlags excludedByAllA = new BooleanFlags(classB.size(), true);
    for (int sa = 0; sa < classA.size(); ++sa) {
      SchemaNode subclassA = classA.get(sa);
      for (int sb = 0; sb < classB.size(); ++sb) {
        SchemaNode subclassB = classB.get(sb);
        if (subclassA.contains(subclassB)) {
          containedBySomeA.makeTrue(sb);
        }
        if (subclassB.contains(subclassA)) {
          containedBySomeB.makeTrue(sa);
        }
        if (!subclassA.excludes(subclassB)) {
          excludedByAllA.makeFalse(sb);
        }
        if (!subclassB.excludes(subclassA)) {
          excludedByAllB.makeFalse(sa);
        }
      }
    }
    boolean classAisRun = classA.get(0) instanceof SchemaRunNode;
    boolean classBisRun = classB.get(0) instanceof SchemaRunNode;
    if (classAisRun && classBisRun) {
      if (containedBySomeB.allTrue()) {
        assert containedBySomeA.allFalse();
        return SequenceRelationship.AllContainedBy;
      } else if ((!containedBySomeB.allFalse())
          && containedBySomeB.xor(excludedByAllB).allTrue()) {
        assert containedBySomeA.allFalse();
        return SequenceRelationship.SomeContainedBy;
      }
      if (containedBySomeA.allTrue()) {
        assert containedBySomeB.allFalse();
        return SequenceRelationship.ContainsAll;
      } else if ((!containedBySomeA.allFalse())
          && containedBySomeA.xor(excludedByAllA).allTrue()) {
        assert containedBySomeB.allFalse();
        return SequenceRelationship.ContainsSome;
      }
      if (excludedByAllB.allTrue() && excludedByAllA.allTrue()) {
        assert containedBySomeB.allFalse();
        assert containedBySomeA.allFalse();
        return SequenceRelationship.Excludes;
      } else if (containedBySomeB.allFalse() && containedBySomeA.allFalse()) {
        return SequenceRelationship.Orthogonal;
      } else {
        String msg =
            "Two run classes in schema (%s and %s) found to violate "
                + "run class ordering requirement.";
        msg = String.format(msg, this.name, other.name);
        throw new SchemaValidationException(msg);
      }
    } else if ((!classAisRun) && (!classBisRun)) {
      return SequenceRelationship.Orthogonal;
    } else if ((!classAisRun) && classBisRun) {
      if (containedBySomeB.allTrue()) {
        return SequenceRelationship.AllContainedBy;
      } else if (containedBySomeB.allFalse()) {
        return SequenceRelationship.NoneContainedBy;
      } else {
        return SequenceRelationship.SomeContainedBy;
      }
    } else if (classAisRun && (!classBisRun)) {
      if (containedBySomeA.allTrue()) {
        return SequenceRelationship.ContainsAll;
      } else if (containedBySomeA.allFalse()) {
        return SequenceRelationship.ContainsNone;
      } else {
        return SequenceRelationship.ContainsSome;
      }
    } else {
      // Unreachable unless the rules of boolean logic have changed
      throw new RuntimeException("Unreachable code.");
    }
  }
}
